/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package com.example.touch.wifipositioning;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

public class FingerPrintingKNN implements IWifiPositioner {

	private final int K;
	private final boolean weighted;

	public FingerPrintingKNN(int k, boolean weighted) {
		this.K = k;
		this.weighted = weighted;
	}

	@Override
	public PositionCalculationResult CalculatePosition(List<TraceEntry> offlineTraces, TraceEntry onlineTrace) {
		HashMap<MACAddress, ArrayList<TraceEntry>> index = new HashMap<MACAddress, ArrayList<TraceEntry>>();

		// Init HashMap with macadresses as keys and a list of entries that
		// 'sees' the macadress as the value
		for (TraceEntry entry : offlineTraces) {
			SignalStrengthSamples s = entry.getSignalStrengthSamples();

			for (MACAddress m : s.getSortedAccessPoints()) {
				if (index.containsKey(m) == false) {
					index.put(m, new ArrayList<TraceEntry>());
				}
				index.get(m).add(entry);
			}
		}

		HashSet<TraceEntry> entries = new HashSet<TraceEntry>();

		for (MACAddress m : onlineTrace.getSignalStrengthSamples().getSortedAccessPoints()) {
			if (index.containsKey(m)) {
				for (TraceEntry offlineEntry : index.get(m)) {
					if (entries.contains(offlineEntry) == false) {
						entries.add(offlineEntry);
					}
				}
			}
		}
		
		Map<TraceEntry, Double> KNearestNeighbours = new HashMap<TraceEntry, Double>();

		// Find K nearest neighbours
		for (TraceEntry offline : entries) {
			double distance = DistanceCalculator.calcDistance(offline, onlineTrace, 4);

			double largestNeighbourDistance = 0;
			TraceEntry neighbourWithLargestDistance = null;

			for (Map.Entry<TraceEntry, Double> neighbour : KNearestNeighbours.entrySet()) {
				if (neighbour.getValue() > largestNeighbourDistance) {
					largestNeighbourDistance = neighbour.getValue();
					neighbourWithLargestDistance = neighbour.getKey();
				}
			}

			if ((distance < largestNeighbourDistance || KNearestNeighbours.size() < K) && distance < 1000) {
				if (neighbourWithLargestDistance != null && KNearestNeighbours.size() >= K) {
					KNearestNeighbours.remove(neighbourWithLargestDistance);
				}
				KNearestNeighbours.put(offline, distance);
			}
		}

		// Add the weighted factor to each neighbors value
		double totalDistance = 0;
		for (Map.Entry<TraceEntry, Double> neighbour : KNearestNeighbours.entrySet()) {
			totalDistance += Math.pow(neighbour.getValue(), 2);
			neighbour.setValue(1 / (Math.pow(neighbour.getValue(), 2)+ +0.0000000000000000000001));
		}
		double totalDistanceRelative = 0;
		for (Map.Entry<TraceEntry, Double> neighbour : KNearestNeighbours.entrySet()) {
			double distanceRelative = neighbour.getValue() * totalDistance;
			totalDistanceRelative += distanceRelative;
			neighbour.setValue(distanceRelative);
		}
		for (Map.Entry<TraceEntry, Double> neighbour : KNearestNeighbours.entrySet()) {
			neighbour.setValue(neighbour.getValue() / totalDistanceRelative+0.0000000000000000000001);
		}

		// Set position based on weighted/unweighted K nearest neigbour
		GeoPosition position = new GeoPosition(0, 0, 0);
		double weightFactor;
		for (Map.Entry<TraceEntry, Double> neighbour : KNearestNeighbours.entrySet()) {
			if (weighted) {
				weightFactor = neighbour.getValue();
			} else {
				weightFactor = (double) 1 / KNearestNeighbours.size();
			}

			position.setX(position.getX() + neighbour.getKey().getPosition().getX() * weightFactor);
			position.setY(position.getY() + neighbour.getKey().getPosition().getY() * weightFactor);
			position.setZ(position.getZ() + neighbour.getKey().getPosition().getZ() * weightFactor);
		}

		PositionCalculationResult result = new PositionCalculationResult(position, onlineTrace.getPosition());

		return result;
	}
}
